﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Android.Content.Res;
using Ultraviolet.Core;
using Ultraviolet.Platform;

namespace Ultraviolet.Shims.Android.Platform
{
    /// <summary>
    /// Represents a <see cref="FileSource"/> which uses the Android asset system.
    /// </summary>
    public class AndroidAssetFileSource : FileSource
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="AndroidAssetFileSource"/> class.
        /// </summary>
        /// <param name="assets">The asset manager.</param>
        /// <param name="assembly">The assembly which contains the asset list.</param>
        public AndroidAssetFileSource(AssetManager assets, Assembly assembly = null)
        {
            Contract.Require(assets, nameof(assets));

            var aalist = default(Dictionary<String, List<AndroidAssetInfo>>);
            var aalistStreamName = assembly?.GetManifestResourceNames().Where(x => x.EndsWith(".AndroidAssets.aalist")).SingleOrDefault();
            if (aalistStreamName != null)
            {
                using (var aalistStream = assembly.GetManifestResourceStream(aalistStreamName))
                    ReadAndroidAssetList(aalistStream, out aalist);
            }

            this.assets = assets;
            this.root = new AndroidAssetFileSourceNode(null, assets, String.Empty, false, aalist);
        }

        /// <inheritdoc/>
        public override FileSourceNode Find(String path, Boolean throwIfNotFound = true)
        {
            Contract.Require(path, nameof(path));

            if (path.StartsWith("/"))
                path = path.Substring(1);

            var components = path.Split(DirectorySeparators);
            var current = root;
            for (var i = 0; i < components.Length; i++)
            {
                var component = components[i];

                if (component == ".")
                    continue;

                if (component == "..")
                {
                    current = (AndroidAssetFileSourceNode)current.Parent;
                    if (current == null)
                    {
                        if (throwIfNotFound)
                        {
                            throw new FileNotFoundException(path);
                        }
                    }
                    continue;
                }

                var match = (AndroidAssetFileSourceNode)current.Children.SingleOrDefault(x =>
                    String.Equals(x.Name, component, StringComparison.Ordinal));

                if (match == null)
                {
                    if (throwIfNotFound)
                    {
                        throw new FileNotFoundException(path);
                    }
                    return null;
                }
                current = match;
            }

            return current;
        }

        /// <inheritdoc/>
        public override Stream Extract(String path)
        {
            Contract.Require(path, nameof(path));

            using (var stream = assets.Open(path))
            {
                var memstream = new MemoryStream();
                stream.CopyTo(memstream);
                memstream.Seek(0, SeekOrigin.Begin);
                return memstream;
            }
        }

        /// <summary>
        /// Reads an Android Asset list generated by the uvassetlist utility.
        /// </summary>
        private static void ReadAndroidAssetList(Stream stream, out Dictionary<String, List<AndroidAssetInfo>> aalist)
        {
            aalist = new Dictionary<String, List<AndroidAssetInfo>>();

            using (var reader = new StreamReader(stream))
            {
                String line;
                while ((line = reader.ReadLine()) != null)
                {
                    var parts = line.Split(':');
                    var isfile = parts[0] == "F";
                    if (isfile && parts.Length != 3)
                        throw new InvalidDataException();
                    if (parts.Length < 2 || parts.Length > 3)
                        throw new InvalidDataException();

                    var key = parts[1];
                    var val = parts.Length > 2 ? parts[2] : null;

                    if (!aalist.TryGetValue(key, out var children))
                    {
                        children = new List<AndroidAssetInfo>();
                        aalist[key] = children;
                    }

                    if (val != null)
                        children.Add(new AndroidAssetInfo(val, isfile));
                }
            }
        }

        // The asset manager.
        private readonly AssetManager assets;
        private readonly AndroidAssetFileSourceNode root;

        // The set of characters used to delimit directories in a path.
        private static readonly Char[] DirectorySeparators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
    }
}